See the question and my original answer on StackOverflow

For what it's worth, this is how I do it (also with the possible combination of a cancellation token):

public static async Task<byte[]> SendReceiveUdpAsync(IPEndPoint endPoint, byte[] packet, int timeout, CancellationToken cancellationToken)
{
    using var client = new UdpClient(endPoint.AddressFamily);
    await client.SendAsync(packet, endPoint, cancellationToken).ConfigureAwait(false);
    var task = client.ReceiveAsync(cancellationToken);
    var index = Task.WaitAny(new [] { task.AsTask() }, timeout, cancellationToken);
    if (index < 0)
        return null;

    return task.Result.Buffer;
}

The trick is to wait for the UDP receive task in a Task.WaitAny call.