It sounds like we’re getting concurrentTimeout in a future release, but meanwhile, this is what I’ve done to get timeouts with std.Io leveraging std.Io.Select. Let me know if I can improve this in any way! After all, this is a lot of boilerplate to accomplish a simple timeout.

    var attachments_arena: std.heap.ArenaAllocator = .init(gpa);
    defer attachments_arena.deinit();

    const SelectT = union(enum) {
        attachments: []const []const u8,
        timeout: void,
    };
    var buf: [2]SelectT = undefined;
    var select: std.Io.Select(SelectT) = .init(io, &buf);
    defer select.cancelDiscard();

    try select.concurrent(.attachments, downloadAttachments, .{ gpa, attachments_arena.allocator(), io, urls });
    try select.concurrent(.timeout, std.Io.sleep, .{ io, .fromSeconds(10), .awake });

    const files: []const []const u8 = switch (try select.await()) {
        .attachments => |attachments| attachments,
        .timeout => if (select.cancel()) |awaited| switch (awaited) {
            .attachments => |attachments| attachments,
            else => &.{},
        } else &.{},
    };

Also some additional feedback with concurrentTimeout (at least based on the proposal):

Adding another version of error.Canceled (error.Timeout) is a bit… annoying. Specifically, there isn’t much meaningful distinction to me, and as a library author, this now means propagating 2 errors everywhere I used to only propagate 1. This kind of propagation is especially annoying in cases where the error isn’t readily available, and I need to grab it from a reader’s state machine… although I guess now that I’ve already done the work, maybe it won’t be too bad. I know Zig has breaking changes nearly every release, but adding a new error to std.Io.Cancelable does not feel “worth it”. In my opinion, a timeout is just a different flavor of a Cancel, and they should just return error.Canceled, as library authors have already built infrastructure specifically for handling that error.

Another thing that could help library authors is the idea of a way to match if an error is part of an error set. Needing to switch (err) { error.Canceled, error.Timeout => |err| return err, ... } is going to get needlessly verbose and entirely defeats the purpose of Zig having great error handling. Something like switch (err) { std.Io.Cancelable => |err| return err, ... } gives library authors a single identifier they can use to say “these are the errors that should be propagated”. It’d also be extremely useful outside of this more specific case, being able to tell if an I/O error was caused by, say, a JSON parse error vs a network stream error.